<!DOCTYPE html>
<html lang='fr'>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>Batocera Web Services</title>
<link rel="shortcut icon" href="favicon.png">
<!-- bootstrap -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/css/bootstrap.min.css" integrity="sha384-Gn5384xqQ1aoWXA+058RXPxPg6fy4IWvTNh0E263XmFcJlSAwiGgFAW/dAiS6JXm" crossorigin="anonymous">
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js" integrity="sha384-KJ3o2DKtIkvYIK3UENzmM7KCkRr/rE9/Qpg6aAZGJwFDMVNA/GpGFF93hXpG5KkN" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.12.9/dist/umd/popper.min.js" integrity="sha384-ApNbgh9B+Y1QKtv3Rn7W3mgPxhU9K/ScQsAP7hUibX39j7fakFPskvXusvfa0b4Q" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@4.0.0/dist/js/bootstrap.min.js" integrity="sha384-JZR6Spejh4U02d8jOt6vLEHfe/JQGiRRSQQxSfFWpi1MquVdAyjUar5+76PVCmYl" crossorigin="anonymous"></script>
<!-- fonts icons -->
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.7.0/css/all.css" integrity="sha384-lZN37f5QGtY3VHgisS14W3ExzMWZxybE1SJSEsQp9S+oqd12jhcu+A56Ebc1zFSJ" crossorigin="anonymous">

<!-- inline CSS -->
<style>
* {
    margin:0px;
    padding:0px;
    font-family: 'Open Sans', sans-serif;
    font-size:12pt;
    color:#FFFFFF;
}

body {
    background-color: #FF3CAC;
    background-image: linear-gradient(125deg, #FF3CAC 0%, #784BA0 50%, #2B86C5 100%);
    background-repeat: no-repeat;
    background-size: cover;
    min-width: 100%;
    min-height: 100vh;
    padding-top: 56px;
    padding-bottom: 120px;
    overflow-x: hidden;
}

.jumbo {
    background-color:#cacbcd;
    padding:4px 4px;
    text-align: center;
}

.main-content {
    overflow-x: hidden;
    overflow-y: auto;
    max-height: calc(100vh - 176px);
    margin-top: 20px;
    padding-top: 10px;
}

.card-title {
    padding:2px 2px;
    text-align:center;
    font-size:10pt;
    text-transform:uppercase;
}

.card-text {
    padding:2px 2px;
    text-align:center;
    font-size:8pt;
}

.modal-title {
    padding:2px 2px;
    text-align:center;
    font-size:12pt;
    text-transform:uppercase;
    color: #000000 !important;
    font-weight: bold;
}

.genre {
    font-size:10px;
    color:#EFEFEF;
    max-width:600px;
    cursor:pointer;
    text-transform:uppercase;
}

.sysName {
    font-size: 12px;
    color:#EFEFEF;
    max-width:600px;
    cursor:pointer;
    text-transform:uppercase;
}

.gameName {
    font-size:14px;
    font-weight:800;
    color:#FFFFFF;
    max-width:600px;
    cursor:pointer;
    text-transform:uppercase;
}

/* control image height */
.card-img-150 {
    max-height: 150px;
    object-fit: contain;
    max-width: 100%;
    width: auto;
    height: auto;
}

.card-img-250 {
    max-height: 250px;
    object-fit: contain;
    max-width: 100%;
    width: auto;
    height: auto;
    display: block;
    margin: 0 auto;
}

.card-title-50 {
    max-height: 50px;
    object-fit: contain;
    max-width: 100%;
    width: auto;
    height: auto;
}

.card-game {
    width: 12rem;
    margin-left: 2px;
    margin-right: 2px;
    margin-top: 2px;
    margin-bottom: 2px;
}

.btn {
    margin-left: 2px;
    margin-right: 2px;
    padding: 5px;
    margin: 5px;
}

.currentborder {
    border: 2px solid #CFCFCF;
    border-radius: 5px;
    padding: 3px;
    margin: 3px;
}

.sys-btn {
    font-size: 52px;
    padding: 2px;
    display: flex;
    text-align: center;
    align-items: center;
    justify-content: center;
    min-height: 70px;
}

.sys-btn .btn-dark {
  width: 100%;
  height: 100%;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  padding: 0.5rem;
  border-radius: 0.5rem;
}

.sys-btn .btn-dark.w-95 {
  width: 95%;
  height: 100%;
}

.sys-btn .btn-dark img {
  max-height: 70px;
  width: auto;
  height: auto;
}

.sys-btn:nth-child(5n+1) { background: #FF3CAC; }
.sys-btn:nth-child(5n+2) { background: #BF9CAC; }
.sys-btn:nth-child(5n+3) { background: #8BD6C5; }
.sys-btn:nth-child(5n+5) { background: #5BA6C5; }
.sys-btn:nth-child(5n+4) { background: #2B86C5; }

.carousel-item-prev,
.carousel-item-next,
.carousel-item.active {
    display:flex;
}

.carousel-control-prev,
.carousel-control-next {
    margin-left: 5px;
    margin-right: 5px;
    background-color: #cf55cf;
    border-radius: 30%;
    width: 40px;
    height: 40px;
}

.carousel-item.next.left,
.carousel-item.prev.right {
    transition: transform .5s ease;
}

.imgArt {
    max-width: 100%;
    height: auto;
}

.modal-content {
    color: #000;
}

.modal-content .card-text {
    color: #000;
}

.modal-body {
    color: #000;
}

.modal-header {
    color: #000;
}

#loading-indicator {
    min-height: 80px;
    display: flex;
    align-items: center;
    justify-content: center;
}

/* Hide loading indicator by default */
#loading-indicator.hidden {
    display: none !important;
}
</style>
</head>

<body>
<!-- See REST Api list on https://github.com/batocera-linux/batocera-emulationstation/blob/master/es-app/src/services/HttpServerThread.cpp#L25 -->
<script type='text/javascript'>

function reloadGamelists() {
    fetch('/reloadgames');
}

function emuKill() {
    fetch('/emukill');
}

function launchGame(game) {
    fetch('/launch', {
        method: 'POST',
        headers: {'Content-Type':'application/x-www-form-urlencoded'},
        body: game
    });
}

function escapeHtml(unsafe) {
    return unsafe
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&#039;");
}

function escapeRom(unsafe) {
    return unsafe
    .replace(/'/g, "");
}

function detailFill(ev) {
    let title = event.target.getAttribute('data-name');
    var mtitle = document.getElementById("detailModalLabel");
    let arg = event.target.getAttribute('data-desc');
    var marg = document.getElementById("detailModalBody");
    let thumb = event.target.getAttribute('data-thumb');
    let img = event.target.getAttribute('data-image');
    var pic = document.getElementById("detailModalImg");
    mtitle.innerHTML = title;
    marg.innerHTML = arg;
    if (img)
        pic.src = img;
    else if (thumb)
        pic.src = thumb;
    else
        pic.src = "";
}

// Virtual scrolling variables
let currentGames = [];
let visibleGames = [];
const GAMES_PER_BATCH = 50;
let currentBatch = 0;
let isLoading = false;
let gameScrollObserver = null;
let currentSystemName = null;
let loadingCancelled = false;

function showGames(arr) {
    currentGames = arr.filter(game => game.hidden !== "true");
    currentBatch = 0;
    visibleGames = [];
    loadingCancelled = false;

    // Clear existing content and observer
    const gameList = document.getElementById("gameList");
    gameList.innerHTML = '';

    if (gameScrollObserver) {
        gameScrollObserver.disconnect();
        gameScrollObserver = null;
    }

    // Create the main container
    gameList.innerHTML = '<div class="row mx-auto col-12 d-flex align-items-stretch justify-content-center flex-wrap" id="gameContainer"></div>';

    // Show initial batch
    loadMoreGames();

    // Set up intersection observer for infinite scroll only if there are more games to load
    if (currentGames.length > GAMES_PER_BATCH) {
        setupInfiniteScroll();
    }
}

function loadMoreGames() {
    if (isLoading || loadingCancelled || currentBatch * GAMES_PER_BATCH >= currentGames.length) {
        return;
    }

    isLoading = true;
    const startIndex = currentBatch * GAMES_PER_BATCH;
    const endIndex = Math.min(startIndex + GAMES_PER_BATCH, currentGames.length);
    const batch = currentGames.slice(startIndex, endIndex);

    // Create game cards
    const gameContainer = document.getElementById("gameContainer");

    batch.forEach(game => {
        if (loadingCancelled) return; // Stop if cancelled
        const cardElement = createGameCardElement(game);
        gameContainer.appendChild(cardElement);
        visibleGames.push(game);
    });

    currentBatch++;
    isLoading = false;

    // Hide loading indicator if all games are loaded
    const loadingIndicator = document.getElementById("loading-indicator");
    if (loadingIndicator && currentBatch * GAMES_PER_BATCH >= currentGames.length) {
        loadingIndicator.classList.add('hidden');
    }
}

function createGameCardElement(game) {
    const regex = /^\d\d\d\d/;
    const cardDiv = document.createElement('div');
    cardDiv.className = 'card text-white bg-dark card-game';

    let cardHtml = '';

    // Image handling
    if(game.thumbnail)
        cardHtml += '<img class="card-img-top card-img-150" loading="lazy" src="' + game.thumbnail + '">';
    else if(game.image)
        cardHtml += '<img class="card-img-top card-img-150" loading="lazy" src="' + game.image + '">';

    // Title/Marquee
    if(game.marquee)
        cardHtml += '<img class="card-img card-title-50" loading="lazy" src="' + game.marquee + '">';
    else
        cardHtml += '<div class="card-title text-center">' + game.name +'</div>';

    cardHtml += '<div class="card-text align-items-center">';

    // Genre
    if (game.genre) {
        cardHtml += '<p>' + game.genre +'</p>';
    }

    // Release date
    if (game.releasedate && regex.test(game.releasedate)) {
        cardHtml += '<p>' + game.releasedate.match(regex) +'</p>';
    }

    // Icons
    if (game.cheevosHash && parseInt(game.cheevosHash) != 0) {
        cardHtml += '<i class="fa fa-trophy" style="margin-left:2px; margin-right:2px"></i>';
    }
    if (game.favorite == "true") {
        cardHtml += '<i class="fa fa-star" style="margin-left:2px; margin-right:2px"></i>';
    }

    // Play count
    if (game.playcount && parseInt(game.playcount) > 0) {
        cardHtml += '<p>Played ' + game.playcount + ' time';
        if (parseInt(game.playcount) > 1)
            cardHtml += 's';
        cardHtml += '</p>';
    } else {
        cardHtml += '<p>Never played</p>';
    }

    cardHtml += '</div><div class="card-footer mx-auto">';
    cardHtml += '<input type="button" class="btn btn-primary" onClick="launchGame(\'' + escapeRom(game.path) + '\')" value="Play">';
    if (game.desc) {
        cardHtml += '<input type="button" class="btn btn-info" data-toggle="modal" data-target="#detailModal" data-desc="' + escapeHtml(game.desc) +
            '" data-name="' + escapeHtml(game.name) +'" data-thumb="' + game.thumbnail +
            '" data-image="' + game.image + '" value="More" onClick="detailFill()">';
    }
    cardHtml += '<br>';
    if (game.manual) {
        cardHtml += '<input type="button" class="btn btn-secondary" value="Manual" onClick="window.open(\'' + game.manual +'\')", "_blank", "noopener,noreferrer"); >';
    }
    if (game.map) {
        cardHtml += '<input type="button" class="btn btn-secondary" value="Map" onClick="window.open(\'' + game.map +'\')", "_blank", "noopener,noreferrer"); >';
    }
    cardHtml += '</div>'; // card-footer

    cardDiv.innerHTML = cardHtml;
    return cardDiv;
}

function setupInfiniteScroll() {
    // Create loading indicator - place it after the game container
    const loadingDiv = document.createElement('div');
    loadingDiv.id = 'loading-indicator';
    loadingDiv.className = 'col-12 text-center p-3';
    loadingDiv.innerHTML = '<div class="spinner-border text-light" role="status"><span class="sr-only">Loading more games...</span></div><p class="text-light mt-2">Loading more games...</p>';

    // Insert the loading indicator after the game container
    const gameContainer = document.getElementById("gameContainer");
    const gameList = document.getElementById("gameList");
    const rowContainer = gameContainer.parentElement;
    rowContainer.appendChild(loadingDiv);

    // Set up intersection observer
    gameScrollObserver = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting && !isLoading && !loadingCancelled && currentBatch * GAMES_PER_BATCH < currentGames.length) {
                // Add a small delay to prevent rapid firing
                setTimeout(() => {
                    if (!isLoading && !loadingCancelled && currentBatch * GAMES_PER_BATCH < currentGames.length) {
                        loadMoreGames();
                    }
                }, 100);
            }
        });
    }, {
        root: null, // Use viewport as root
        rootMargin: '100px', // Start loading when 100px before the element comes into view
        threshold: 0.1
    });

    gameScrollObserver.observe(loadingDiv);
}

function getOrder(prop) {
    return function(a, b) {
        if (a[prop] > b[prop]) {
            return 1;
        } else if (a[prop] < b[prop]) {
            return -1;
        }
        return 0;
    }
}

function loadGames(name) {
    // Cancel any ongoing loading
    loadingCancelled = true;
    currentSystemName = name;

    // Disconnect existing observer immediately
    if (gameScrollObserver) {
        gameScrollObserver.disconnect();
        gameScrollObserver = null;
    }

    // Show loading state
    document.getElementById("gameList").innerHTML = '<div class="text-center p-5"><div class="spinner-border text-light" role="status"><span class="sr-only">Loading...</span></div><p class="mt-3">Loading games...</p></div>';

    var xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (this.readyState == 4 && this.status == 200) {
            // Only process if this is still the current system
            if (currentSystemName === name) {
                var myArr = JSON.parse(this.responseText);
                myArr.sort(getOrder("name"));
                showGames(myArr);
            }
        }
    };
    xhr.open('GET', '/systems/' + name + '/games');
    xhr.send();
}

function removeItem(arr, value) {
    var index = arr.indexOf(value);
    if (index > -1) {
        arr.splice(index, 1);
    }
    return arr;
}

function showSystems(arr) {
    const w = window.innerWidth || document.documentElement.clientWidth;
    var nb_sys = 12;
    var sz_sys = "col-2";
    if (w < 1200) { nb_sys = 8; sz_sys = "col-3" };
    if (w < 900)  { nb_sys = 6; sz_sys = "col-4" };

    var out = '<div class="carousel slide" id="sysCarousel" data-touch="true" data-interval="false" data-ride="carousel">' +
        '<div class="carousel-inner">';
    var i;
    // Filter visible systems
    arr = arr.filter(function(system) {
        return system.visible == "true";
    });

    for(i = 0; i < arr.length; i++) {
        if ( i % nb_sys == 0 ) {
            if ( i == 0 )
                out += '<div class="carousel-item active">';
            else
                out += '<div class="carousel-item">';
            out += '<div class="row">';
        }
        if (arr[i].logo) {
            out += '<div class="'+ sz_sys +' sys-btn"><a onclick="javascript:loadGames(\''+ arr[i].name +'\')" href="#"><button type="button" class="btn btn-dark w-95"><img style="width:60pt;" src="' + arr[i].logo + '"></button></a></div>';
        } else {
            out += '<div class="'+ sz_sys +' sys-btn"><a onclick="javascript:loadGames(\''+ arr[i].name +'\')" href="#"><button type="button" class="btn btn-dark w-95">' + arr[i].fullname + '</button></a></div>';
        }
        if ( i % nb_sys == (nb_sys - 1) || i == arr.length - 1)
            out += '</div></div>' ; //  row carousel-item
    }

    out += '</div>'; // carousel-inner
    out += '<button class="carousel-control-prev btn-sm btn-outline-info" type="button" data-target="#sysCarousel" data-slide="prev">' +
        '  <span class="carousel-control-prev-icon" ></span>' +
        '</button>' +
        '<button class="carousel-control-next btn-sm btn-outline-info" type="button" data-target="#sysCarousel" data-slide="next">' +
        '  <span class="carousel-control-next-icon" ></span>' +
        '</button>' +
        '</div>'; // carousel container
    document.getElementById("systemList").innerHTML = out;
}

function describe(d) {
    var out = "";

    if (d.msg) {
        if (d.msg == "NO GAME RUNNING") {
            out += '<div class="gameName" id="gameName">No game running</div>';
            return (out);
        }
        if (d.msg == "ERROR") {
            out += '<div class="gameName" id="gameName">Notification service unreachable</div>';
            return (out);
        }
    }
    out += '<div class="row mx-auto">';
    if (d.image) {
        out += '<div class="col-4 float-right imgCont"><img src="' + d.image + '" class="imgArt" id="imgArt" width="64"></div>';
    } else if (d.thumbnail) {
        out += '<div class="col-4 float-right imgCont"><img src="' + d.thumbnail + '" class="imgArt" id="imgArt" width="64"></div>';
    }
    out += '<div class="col-8 sysCont">';
    if (d.systemName) {
        var url = "/systems/" + d.systemName + "/logo";
        fetch(url).then(response => response.blob())
            .then((blob) => {
                const imgE = document.createElement('img');
                const imgUrl = URL.createObjectURL(blob);
                imgE.src = imgUrl;
                imgE.style.height = "24pt";
                const container = document.getElementById("sysName");
                if (container) {
                    container.innerHTML='';
                    container.appendChild(imgE);
                }
            });
        out += '<div class="row no-gutters"><div class="col-12 sysName" id="sysName" title="">' + d.systemName +'</div></div>';
    }
    if (d.name) {
        out += '<div class="row no-gutters"><div class="col-12 gameName" id="gameName" title="">' + d.name +'</div></div>';
    }
    out += '</div></div>';
    return (out);
}

var PREVDATA = null;

$(document).ready(function() {
    fetch('/systems')
    .then(response => response.json())
    .then(data => {
        showSystems(data);
    });

    var latestOutput = document.getElementById("jumbo");
    // Construct the WebSocket URL based on the current location of the web page
    var wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
    var wsHost = location.host;
    wsHost = wsHost.split(':')[0]+":8080"
    var wsUrl = wsProtocol + '//' + wsHost + '/';
    var ws = new WebSocket(wsUrl);
    ws.onmessage = function(event) {
        var d = event.data;
        if (PREVDATA != d) {
            var dd = JSON.parse(d);
            latestOutput.innerHTML = describe(dd);
            PREVDATA = d;
        }
    };
    ws.onclose = function() {
        var message = { "msg": "ERROR" };
        latestOutput.innerHTML = describe(message);
    };
});

</script>

<!-- Modal windows -->
<div class="modal fade" id="killModal" tabindex="-1" role="dialog" aria-labelledby="KillModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="KillModalLabel">Are you sure?</h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
Confirm you want to kill the running emulator and game.
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Cancel</button> <input type="button" class="btn btn-danger" data-dismiss="modal" onclick="emuKill()" value="Kill it!">
</div>
</div>
</div>
</div>

<div class="modal fade" id="detailModal" tabindex="-1" role="dialog" aria-labelledby="detailModalLabel" aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="detailModalLabel"></h5><button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">×</span></button>
</div>
<div class="modal-body">
<img class="card-img-top card-img-250" loading="lazy" src="" id="detailModalImg">
<div class="card-body">
<p class="card-text" id="detailModalBody"></p>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
</div>
</div>
</div>
</div>

<!-- navbars -->
<nav class="navbar navbar-expand navbar-dark bg-dark fixed-top">
<div class="flex-column mx-auto">
<ul class="navbar-nav">
<li class="nav-item">
<a class="navbar-brand" href="#"><img src="logo-tri.png" height="28"></a>
</li>
<li class="nav-item"><input type="button" class="btn btn-primary" data-toggle="collapse" data-target="#systemList" aria-expanded="true" aria-controls="systemList" id="showSystems" value="Systems"></li>
<li class="nav-item"><button type="button" class="btn btn-secondary" data-toggle="collapse" data-target="#jumbo" id="showNotifs">Current</button></li>
<li class="nav-item"><input type="button" class="btn btn-info" value="Reload" onclick='reloadGamelists()'></li>
<li class="nav-item"><button type="button" class="btn btn-danger" data-toggle="modal" data-target="#killModal">Kill</button></li>
</ul>
<ul class="navbar-nav">
<li class="nav-item mx-auto">
<div class="container-fluid collapse show currentborder" id="jumbo"></div>
</li>
</ul>
</div>
</nav>

<div class="main-content">
<div class="row">
<div id="gameList"></div>
</div>
</div>

<nav class="navbar navbar-expand navbar-dark bg-dark fixed-bottom">
<div class="mx-auto">
<div id="systemList" class="collapse show"></div>
</div>
</nav>

</body>
</html>
